﻿// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;

namespace SnapObjects.Data
{
    internal class SystJsonNodeWriter : INodeWriter, IDisposable
    {
        private string _propertyName;
        private readonly Utf8JsonWriter _writer;
        private readonly IndexStack<JsonNodeType> _levels;
        private readonly Stream _stream;
        private readonly bool _innerStream = false;

        public SystJsonNodeWriter() : this(new MemoryStream())
        {
            _innerStream = true;
        }

        public SystJsonNodeWriter(Stream stream)
        {
            _stream = stream;
            var options = new JsonWriterOptions() { Indented = false, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
            _writer = new Utf8JsonWriter(_stream, options);

            _levels = new IndexStack<JsonNodeType>();
        }

        public INodeWriter WriteAsArray(string name)
        {
            _propertyName = name;

            if (!String.IsNullOrWhiteSpace(name))
            {
                this.WritePropertyName(name);
            }

            _writer.WriteStartArray();

            _levels.Push(JsonNodeType.Array);

            return this;
        }

        public INodeWriter WriteAsObject(string name)
        {
            _propertyName = name;

            if (!String.IsNullOrWhiteSpace(name))
            {
                this.WritePropertyName(name);
            }

            _writer.WriteStartObject();

            _levels.Push(JsonNodeType.Object);

            return this;
        }

        public INodeWriter WriteAsValue(string name)
        {
            _propertyName = name;

            if (_levels.Peek() != JsonNodeType.Array)
            {
                if (String.IsNullOrWhiteSpace(name))
                {
                    throw new ArgumentNullException(nameof(name));
                }
            }

            return this;
        }

        public void WriteEndArray()
        {
            _writer.WriteEndArray();

            _levels.Pop();

            _propertyName = null;
        }

        public void WriteEndObject()
        {
            _writer.WriteEndObject();

            _levels.Pop();

            _propertyName = null;
        }

        public void WriteNull()
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteNull(_propertyName);
            }
            else
            {
                _writer.WriteNullValue();
            }
        }

        public void WriteRawValue(string raw)
        {
            if (raw == null)
            {
                this.WriteNull();
            }
            else
            {
                JsonDocument jsonDoc = null;
                try
                {
                    jsonDoc = JsonDocument.Parse(raw);
                }
                catch
                {
                    //TODO...
                }

                if (jsonDoc != null)
                {
                    SysJsonWriterHelper.WriteJsonElement(this, _propertyName, jsonDoc.RootElement);
                }
                else
                {
                    this.WriteString(raw);
                }
            }
        }

        public void WriteValue(object value)
        {
            if (value == null || value == DBNull.Value)
            {
                this.WriteNull();

                return;
            }

            switch (value)
            {
                case bool boolValue:
                    this.WriteBool(boolValue);
                    break;
                case byte byteValue:
                    this.WriteByte(byteValue);
                    break;
                case char charValue:
                    this.WriteString(charValue.ToString());
                    break;
                case decimal decValue:
                    this.WriteDecimal(decValue);
                    break;
                case double doubleValue:
                    //this.WriteDecimal(Convert.ToDecimal(doubleValue));
                    this.WriteDouble(doubleValue);
                    break;
                case short shortValue:
                    this.WriteShort(shortValue);
                    break;
                case int intValue:
                    this.WriteInteger(intValue);
                    break;
                case long longValue:
                    this.WriteLong(longValue);
                    break;
                case sbyte sbyteValue:
                    this.WriteSbyte(sbyteValue);
                    break;
                case float floatValue:
                    //this.WriteDecimal(Convert.ToDecimal(floatValue));
                    this.WriteFloat(floatValue);
                    break;
                case ushort ushortValue:
                    this.WriteUshort(ushortValue);
                    break;
                case uint uintValue:
                    this.WriteUInteger(uintValue);
                    break;
                case ulong ulongValue:
                    this.WriteUlong(ulongValue);
                    break;
                case Enum enumValue:
                    this.WriteUlong(Convert.ToUInt64(enumValue));
                    break;
                case DateTime dateTime:
                    this.WriteDateTime(dateTime);
                    break;
                case DateTimeOffset dateTimeOffset:
                    this.WriteDateTimeOffset(dateTimeOffset);
                    break;
                case byte[] byts:
                    this.WriteBytes(byts);
                    break;
                default:
                    this.WriteString(value.ToString());
                    break;
            }
        }

        private void WritePropertyName(string name)
        {
            if (_levels.IsEmpty() || _levels.Peek() != JsonNodeType.Object)
            {
                return;
            }

            _writer.WritePropertyName(name);
        }

        private bool IsWritePropertyValue()
        {
            return _levels.Peek() != JsonNodeType.Array;
        }

        private void WriteDateTime(DateTime value)
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteString(_propertyName, value);
            }
            else
            {
                _writer.WriteStringValue(value);
            }
        }

        private void WriteDateTimeOffset(DateTimeOffset value)
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteString(_propertyName, value);
            }
            else
            {
                _writer.WriteStringValue(value);
            }
        }

        private void WriteBool(bool value)
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteBoolean(_propertyName, value);
            }
            else
            {
                _writer.WriteBooleanValue(value);
            }
        }

        private void WriteByte(byte value)
        {
            this.WriteInteger(value);
        }

        private void WriteSbyte(sbyte value)
        {
            this.WriteInteger(value);
        }

        private void WriteString(string value)
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteString(_propertyName, value);
            }
            else
            {
                _writer.WriteStringValue(value);
            }
        }

        private void WriteInteger(int value)
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteNumber(_propertyName, value);
            }
            else
            {
                _writer.WriteNumberValue(value);
            }
        }

        private void WriteUInteger(uint value)
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteNumber(_propertyName, value);
            }
            else
            {
                _writer.WriteNumberValue(value);
            }
        }

        private void WriteLong(long value)
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteNumber(_propertyName, value);
            }
            else
            {
                _writer.WriteNumberValue(value);
            }
        }

        private void WriteUlong(ulong value)
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteNumber(_propertyName, value);
            }
            else
            {
                _writer.WriteNumberValue(value);
            }
        }

        private void WriteShort(short value)
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteNumber(_propertyName, value);
            }
            else
            {
                _writer.WriteNumberValue(value);
            }
        }

        private void WriteUshort(ushort value)
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteNumber(_propertyName, value);
            }
            else
            {
                _writer.WriteNumberValue(value);
            }
        }

        private void WriteDouble(double value)
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteNumber(_propertyName, value);
            }
            else
            {
                _writer.WriteNumberValue(value);
            }
        }

        private void WriteDecimal(decimal value)
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteNumber(_propertyName, value);
            }
            else
            {
                _writer.WriteNumberValue(value);
            }
        }

        private void WriteFloat(float value)
        {
            var rawValue = value.ToString("G17");

            if (this.IsWritePropertyValue())
            {
                _writer.WritePropertyName(_propertyName);
                _writer.WriteRawValue(rawValue);
            }
            else
            {
                _writer.WriteRawValue(rawValue);
            }
        }

        private void WriteBytes(byte[] bytes)
        {
            if (this.IsWritePropertyValue())
            {
                _writer.WriteBase64String(_propertyName, bytes);
            }
            else
            {
                _writer.WriteBase64StringValue(bytes);
            }
        }

        public void Flush()
        {
            _writer.Flush();
        }

        public override string ToString()
        {
            this.Flush();

            _stream.Position = 0L;
            var reader = new StreamReader(_stream);
            return reader.ReadToEnd();
        }

        public void Dispose()
        {
            if (_innerStream)
            {
                _stream.Close();
                _stream.Dispose();
            }

            _writer.Dispose();
        }

        private enum JsonNodeType
        {
            None = 0,
            Object,
            Array,
            Property
        }
    }
}